web/src/routes/image/[id]/+page.svelte (view raw)
1<script lang="ts">
2 import { onMount } from 'svelte';
3 import { getImage, imageDownloadUrl, thumbnailUrl, type ImageDetail } from '$lib/api';
4
5 let { params } = $props();
6 let id = $derived(parseInt(params.id));
7
8 let img = $state<ImageDetail | null>(null);
9 let loading = $state(true);
10 let error = $state('');
11
12 onMount(async () => {
13 try {
14 img = await getImage(id);
15 } catch (err) {
16 error = err instanceof Error ? err.message : 'Failed to load image';
17 } finally {
18 loading = false;
19 }
20 });
21
22 function formatSize(bytes: number): string {
23 if (bytes < 1024) return bytes + ' B';
24 if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
25 return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
26 }
27
28 function formatDate(s: string): string {
29 return new Date(s).toLocaleString();
30 }
31</script>
32
33{#if loading}
34 <p>Loading...</p>
35{:else if error}
36 <p class="error">{error}</p>
37{:else if img}
38 <h1>{img.filename}</h1>
39
40 <div class="detail">
41 <div class="preview">
42 {#if img.thumbnail_path}
43 <a href={imageDownloadUrl(img.id)} download={img.filename}>
44 <img src={thumbnailUrl(img.id)} alt={img.filename} />
45 </a>
46 {:else}
47 <div class="placeholder">No thumbnail</div>
48 {/if}
49 </div>
50
51 <div class="meta">
52 <dl>
53 <dt>Dimensions</dt>
54 <dd>{img.width} × {img.height}</dd>
55 <dt>File size</dt>
56 <dd>{formatSize(img.file_size)}</dd>
57 <dt>Type</dt>
58 <dd>{img.mime_type}</dd>
59 <dt>Tags</dt>
60 <dd>
61 {#if img.tags_detail.length > 0}
62 <div class="tag-pills">
63 {#each img.tags_detail as t (t.tag)}
64 <a href="/browse?tag={encodeURIComponent(t.tag)}" class="tag-pill" title="Used {t.usage_count} time{t.usage_count === 1 ? '' : 's'}">
65 <span class="tag-name">{t.tag}</span>
66 <span class="tag-count">{t.usage_count}</span>
67 </a>
68 {/each}
69 </div>
70 {:else}
71 <span class="dim">none</span>
72 {/if}
73 </dd>
74 <dt>Uploaded</dt>
75 <dd>{formatDate(img.created_at)}</dd>
76 </dl>
77
78 <a href={imageDownloadUrl(img.id)} class="download" download={img.filename}>
79 Download original
80 </a>
81 </div>
82 </div>
83
84 <p class="back"><a href="/browse">← Back to browse</a></p>
85{/if}
86
87<style>
88 .detail {
89 display: flex;
90 gap: 2rem;
91 margin-top: 1rem;
92 flex-wrap: wrap;
93 }
94 .preview img {
95 max-width: 100%;
96 max-height: 60vh;
97 border-radius: 8px;
98 box-shadow: 0 2px 8px rgba(0,0,0,0.15);
99 }
100 .placeholder {
101 width: 300px;
102 height: 200px;
103 display: flex;
104 align-items: center;
105 justify-content: center;
106 background: #f0f0f0;
107 border-radius: 8px;
108 color: #999;
109 }
110 .meta {
111 min-width: 220px;
112 }
113 dl {
114 margin: 0;
115 }
116 dt {
117 font-weight: 600;
118 margin-top: 0.5rem;
119 font-size: 0.85rem;
120 color: #555;
121 }
122 dd {
123 margin: 0;
124 font-size: 0.95rem;
125 }
126 .dim {
127 color: #999;
128 }
129 .download {
130 display: inline-block;
131 margin-top: 1.5rem;
132 padding: 0.5rem 1.5rem;
133 background: #1a1a2e;
134 color: #eee;
135 text-decoration: none;
136 border-radius: 6px;
137 }
138 .download:hover {
139 background: #16213e;
140 }
141 .back {
142 margin-top: 2rem;
143 }
144
145 .tag-pills {
146 display: flex;
147 flex-wrap: wrap;
148 gap: 0.4rem;
149 margin-top: 0.25rem;
150 }
151 .tag-pill {
152 display: inline-flex;
153 align-items: center;
154 gap: 0.35rem;
155 background: #1a1a2e;
156 border-radius: 20px;
157 padding: 0.3rem 0.65rem 0.3rem 0.8rem;
158 font-size: 0.85rem;
159 color: #eee;
160 text-decoration: none;
161 white-space: nowrap;
162 transition: background 0.15s;
163 }
164 .tag-pill:hover {
165 background: #16213e;
166 }
167 .tag-name {
168 font-weight: 500;
169 }
170 .tag-count {
171 background: rgba(255, 255, 255, 0.15);
172 border-radius: 10px;
173 padding: 0.06rem 0.4rem;
174 font-size: 0.72rem;
175 font-weight: 600;
176 color: rgba(255, 255, 255, 0.75);
177 min-width: 1.2rem;
178 text-align: center;
179 }
180
181 .error {
182 color: #c0392b;
183 }
184</style>